Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
378 / 378
Serializer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
13 / 13
89
100.00% covered (success)
100.00%
378 / 378
 convertAttributeValueTemplate($attrValue)
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
9 / 9
 convertCondition($expr)
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
18 / 18
 convertXPath($expr)
100.00% covered (success)
100.00%
1 / 1
46
100.00% covered (success)
100.00%
213 / 213
 serializeApplyTemplates(DOMElement $applyTemplates)
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 serializeAttribute(DOMElement $attribute)
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 serializeChildren(DOMElement $ir)
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
6 / 6
 serializeCloseTag(DOMElement $closeTag)
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
36 / 36
 serializeComment(DOMElement $comment)
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 serializeCopyOfAttributes(DOMElement $copyOfAttributes)
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 serializeElement(DOMElement $element)
100.00% covered (success)
100.00%
1 / 1
11
100.00% covered (success)
100.00%
42 / 42
 serializeMatch()
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 serializeOutput(DOMElement $output)
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
15 / 15
 serializeSwitch(DOMElement $switch)
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
17 / 17
<?php
/**
* @package   s9e\TextFormatter
* @copyright Copyright (c) 2010-2014 The s9e Authors
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
use DOMElement;
use DOMXPath;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
class Serializer
{
    /**
    * @var array Custom XPath representations as [xpath => php]
    */
    protected $customXPath = [
        // BBcodes: LIST
        "contains('upperlowerdecim',substring(@type,1,5))"
            => "strpos('upperlowerdecim',substr(\$node->getAttribute('type'),0,5))!==false",
        // MediaEmbed: Bandcamp
        '120-78*boolean(@track_id|@track_num)'
            => "(\$node->hasAttribute('track_id')||\$node->hasAttribute('track_num')?42:120)",
        // MediaEmbed: Grooveshark
        "substring('songWw',6-5*boolean(@songid),5)"
            => "(\$node->hasAttribute('songid')?'songW':'w')",
        "250-210*boolean(@songid)"
            => "(\$node->hasAttribute('songid')?40:250)",
        // MediaEmbed: Spotify
        "380-300*(contains(@uri,':track:')orstarts-with(@path,'track/'))"
            => "(strpos(\$node->getAttribute('uri'),':track:')!==false||strpos(\$node->getAttribute('path'),'track/')===0?80:380)",
        // MediaEmbed: Twitch
        "substring('archl',5-4*boolean(@archive_id|@chapter_id),4)"
            => "(\$node->hasAttribute('archive_id')||\$node->hasAttribute('chapter_id')?'arch':'l')"
    ];
    /**
    * @var string Output method
    */
    public $outputMethod = 'html';
    /**
    * @var bool Whether to use the mbstring functions as a replacement for XPath expressions
    */
    public $useMultibyteStringFunctions = false;
    /**
    * Convert an attribute value template into PHP
    *
    * NOTE: escaping must be performed by the caller
    *
    * @link http://www.w3.org/TR/xslt#dt-attribute-value-template
    *
    * @param  string $attrValue Attribute value template
    * @return string
    */
    protected function convertAttributeValueTemplate($attrValue)
    {
        $phpExpressions = [];
        foreach (TemplateHelper::parseAttributeValueTemplate($attrValue) as $token)
        {
            if ($token[0] === 'literal')
            {
                $phpExpressions[] = var_export($token[1], true);
            }
            else
            {
                $phpExpressions[] = $this->convertXPath($token[1]);
            }
        }
        return implode('.', $phpExpressions);
    }
    /**
    * Convert an XPath expression (used in a condition) into PHP code
    *
    * This method is similar to convertXPath() but it selectively replaces some simple conditions
    * with the corresponding DOM method for performance reasons
    *
    * @param  string $expr XPath expression
    * @return string       PHP code
    */
    public function convertCondition($expr)
    {
        $expr = trim($expr);
        // XSL: <xsl:if test="@foo">
        // PHP: if ($node->hasAttribute('foo'))
        if (preg_match('#^@([-\\w]+)$#', $expr, $m))
        {
            return '$node->hasAttribute(' . var_export($m[1], true) . ')';
        }
        // XSL: <xsl:if test="not(@foo)">
        // PHP: if (!$node->hasAttribute('foo'))
        if (preg_match('#^not\\(@([-\\w]+)\\)$#', $expr, $m))
        {
            return '!$node->hasAttribute(' . var_export($m[1], true) . ')';
        }
        // XSL: <xsl:if test="$foo">
        // PHP: if (!empty($this->params['foo']))
        if (preg_match('#^\\$(\\w+)$#', $expr, $m))
        {
            return '!empty($this->params[' . var_export($m[1], true) . '])';
        }
        // XSL: <xsl:if test="not($foo)">
        // PHP: if (empty($this->params['foo']))
        if (preg_match('#^not\\(\\$(\\w+)\\)$#', $expr, $m))
        {
            return 'empty($this->params[' . var_export($m[1], true) . '])';
        }
        // If the condition does not seem to contain a relational expression, or start with a
        // function call, we wrap it inside of a boolean() call
        if (!preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr))
        {
            // XSL: <xsl:if test="parent::foo">
            // PHP: if ($this->xpath->evaluate("boolean(parent::foo)",$node))
            $expr = 'boolean(' . $expr . ')';
        }
        // XSL: <xsl:if test="@foo='bar'">
        // PHP: if ($this->xpath->evaluate("@foo='bar'",$node))
        return $this->convertXPath($expr);
    }
    /**
    * Convert an XPath expression (used as value) into PHP code
    *
    * @param  string $expr XPath expression
    * @return string       PHP code
    */
    public function convertXPath($expr)
    {
        static $regexp;
        $expr = trim($expr);
        // Use the custom representation if applicable
        if (isset($this->customXPath[$expr]))
        {
            return $this->customXPath[$expr];
        }
        if (!isset($regexp))
        {
            $patterns = [
                'attr'      => ['@', '(?<attrName>[-\\w]+)'],
                'dot'       => '\\.',
                'not'       => ['not', '\\(', '(?&value)', '\\)'],
                'name'      => 'name\\(\\)',
                'lname'     => 'local-name\\(\\)',
                'param'     => ['\\$', '(?<paramName>\\w+)'],
                'string'    => '"[^"]*"|\'[^\']*\'',
                'number'    => ['-?', '\\d++'],
                'strlen'    => ['string-length', '\\(', '(?<strlen0>(?&value))?', '\\)'],
                'contains'  => [
                    'contains',
                    '\\(',
                    '(?<contains0>(?&value))',
                    ',',
                    '(?<contains1>(?&value))',
                    '\\)'
                ],
                'translate' => [
                    'translate',
                    '\\(',
                    '(?<translate0>(?&value))',
                    ',',
                    '(?<translate1>(?&string))',
                    ',',
                    '(?<translate2>(?&string))',
                    '\\)'
                ],
                'substr'    => [
                    'substring',
                    '\\(',
                    '(?<substr0>(?&value))',
                    ',',
                    '(?<substr1>(?&value))',
                    '(?:, (?<substr2>(?&value)))?',
                    '\\)'
                ],
                'startswith'  => [
                    'starts-with',
                    '\\(',
                    '(?<startswith0>(?&value))',
                    ',',
                    '(?<startswith1>(?&value))',
                    '\\)'
                ]
            ];
            $exprs = [];
            // Create a regexp that matches values, such as "@foo" or "42"
            $valueExprs = [];
            foreach ($patterns as $name => $pattern)
            {
                if (is_array($pattern))
                {
                    $pattern = implode(' ', $pattern);
                }
                $valueExprs[] = '(?<' . $name . '>' . $pattern . ')';
            }
            $exprs[] = '(?<value>' . implode('|', $valueExprs) . ')';
            // Create a regexp that matches a comparison such as "@foo = 1"
            // NOTE: cannot support < or > because of NaN -- (@foo<5) returns false if @foo=''
            $exprs[] = '(?<cmp>(?<cmp0>(?&value)) (?<cmp1>!?=) (?<cmp2>(?&value)))';
            // Match parenthesized expressions on PCRE >= 8.13, previous versions segfault
            // because of the mutual references
            $parensMatch = '';
            if (version_compare(PCRE_VERSION, '8.13', '>='))
            {
                $parensMatch = '|(?&parens)';
                // Create a regexp that matches a parenthesized expression
                // NOTE: could be expanded to support any expression
                $exprs[] = '(?<parens>\\( (?<parens0>(?&bool)|(?&cmp)) \\))';
            }
            // Create a regexp that matches boolean operations
            $exprs[] = '(?<bool>(?<bool0>(?&cmp)|(?&value)' . $parensMatch . ') (?<bool1>and|or) (?<bool2>(?&cmp)|(?&value)|(?&bool)' . $parensMatch . '))';
            // Assemble the final regexp
            $regexp = '#^(?:' . implode('|', $exprs) . ')$#S';
            // Replace spaces with any amount of whitespace
            $regexp = str_replace(' ', '\\s*', $regexp);
        }
        if (preg_match($regexp, $expr, $m))
        {
            if (!empty($m['attrName']))
            {
                // XSL: <xsl:value-of select="@foo"/>
                // PHP: $this->out .= $node->getAttribute('foo');
                return '$node->getAttribute(' . var_export($m['attrName'], true) . ')';
            }
            // XSL: <xsl:value-of select="."/>
            // PHP: $this->out .= $node->textContent;
            if (!empty($m['dot']))
            {
                return '$node->textContent';
            }
            // XSL: <xsl:value-of select="$foo"/>
            // PHP: $this->out .= $this->params['foo'];
            if (!empty($m['paramName']))
            {
                return '$this->params[' . var_export($m['paramName'], true) . ']';
            }
            // XSL: <xsl:value-of select="'foo'"/>
            // XSL: <xsl:value-of select='"foo"'/>
            // PHP: $this->out .= 'foo';
            if (!empty($m['string']))
            {
                return var_export(substr($m['string'], 1, -1), true);
            }
            // XSL: <xsl:value-of select="local-name()"/>
            // PHP: $this->out .= $node->localName;
            if (!empty($m['lname']))
            {
                return '$node->localName';
            }
            // XSL: <xsl:value-of select="name()"/>
            // PHP: $this->out .= $node->nodeName;
            if (!empty($m['name']))
            {
                return '$node->nodeName';
            }
            // XSL: <xsl:value-of select="3"/>
            // PHP: $this->out .= '3';
            if (!empty($m['number']))
            {
                return "'" . $expr . "'";
            }
            // XSL: <xsl:value-of select="string-length(@foo)"/>
            // PHP: $this->out .= mb_strlen($node->getAttribute('foo'),'utf-8');
            if (!empty($m['strlen']) && $this->useMultibyteStringFunctions)
            {
                if (!isset($m['strlen0']))
                {
                    $m['strlen0'] = '.';
                }
                return 'mb_strlen(' . $this->convertXPath($m['strlen0']) . ",'utf-8')";
            }
            // XSL: <xsl:value-of select="substring(@foo, 1, 2)"/>
            // PHP: $this->out .= mb_substring($node->getAttribute('foo'),0,2,'utf-8');
            //
            // NOTE: negative values for the second argument do not produce the same result as
            //       specified in XPath if the argument is not a literal number
            if (!empty($m['substr']) && $this->useMultibyteStringFunctions)
            {
                $php = 'mb_substr(' . $this->convertXPath($m['substr0']) . ',';
                // Hardcode the value if possible
                if (preg_match('#^\\d+$#D', $m['substr1']))
                {
                    $php .= max(0, $m['substr1'] - 1);
                }
                else
                {
                    $php .= 'max(0,' . $this->convertXPath($m['substr1']) . '-1)';
                }
                $php .= ',';
                if (isset($m['substr2']))
                {
                    if (preg_match('#^\\d+$#D', $m['substr2']))
                    {
                        // Handles substring(0,2) as per XPath
                        if (preg_match('#^\\d+$#D', $m['substr1']) && $m['substr1'] < 1)
                        {
                            $php .= max(0, $m['substr1'] + $m['substr2'] - 1);
                        }
                        else
                        {
                            $php .= max(0, $m['substr2']);
                        }
                    }
                    else
                    {
                        $php .= 'max(0,' . $this->convertXPath($m['substr2']) . ')';
                    }
                }
                else
                {
                    $php .= 'null';
                }
                $php .= ",'utf-8')";
                return $php;
            }
            if (!empty($m['contains']))
            {
                return '(strpos(' . $this->convertXPath($m['contains0']) . ',' . $this->convertXPath($m['contains1']) . ')!==false)';
            }
            if (!empty($m['startswith']))
            {
                return '(strpos(' . $this->convertXPath($m['startswith0']) . ',' . $this->convertXPath($m['startswith1']) . ')===0)';
            }
            if (!empty($m['cmp1']))
            {
                $operators = [
                    '='  => '===',
                    '!=' => '!==',
                    '>'  => '>',
                    '>=' => '>=',
                    '<'  => '<',
                    '<=' => '<='
                ];
                // If either operand is a number, represent it as a PHP number and replace the
                // identity operators
                foreach (['cmp0', 'cmp2'] as $k)
                {
                    if (preg_match('#^\\d+$#', $m[$k]))
                    {
                        $operators['=']  = '==';
                        $operators['!='] = '!=';
                        $m[$k] = ltrim($m[$k], '0');
                    }
                    else
                    {
                        $m[$k] = $this->convertXPath($m[$k]);
                    }
                }
                return $m['cmp0'] . $operators[$m['cmp1']] . $m['cmp2'];
            }
            if (!empty($m['bool1']))
            {
                $operators = [
                    'and' => '&&',
                    'or'  => '||'
                ];
                return $this->convertCondition($m['bool0']) . $operators[$m['bool1']] . $this->convertCondition($m['bool2']);
            }
            if (!empty($m['parens']))
            {
                return '(' . $this->convertXPath($m['parens0']) . ')';
            }
            if (!empty($m['translate']))
            {
                preg_match_all('/./u', substr($m['translate1'], 1, -1), $matches);
                $from = $matches[0];
                preg_match_all('/./u', substr($m['translate2'], 1, -1), $matches);
                $to = $matches[0];
                // We adjust $to to match the number of elements in $from, either by truncating it
                // or by padding it with empty strings
                if (count($to) > count($from))
                {
                    $to = array_slice($to, 0, count($from));
                }
                else
                {
                    // NOTE: we don't use array_merge() because of potential side-effects when
                    //       translating digits
                    while (count($from) > count($to))
                    {
                        $to[] = '';
                    }
                }
                // Remove duplicates in $from, as well as the corresponding elements in $to
                $from = array_unique($from);
                $to   = array_intersect_key($to, $from);
                // Start building the strtr() call
                $php = 'strtr(' . $this->convertXPath($m['translate0']) . ',';
                // Test whether all elements in $from and $to are exactly 1 byte long, meaning they
                // are ASCII and with no empty strings. If so, we can use the scalar version of
                // strtr(), otherwise we have to use the array version
                if ([1] === array_unique(array_map('strlen', $from))
                 && [1] === array_unique(array_map('strlen', $to)))
                {
                    $php .= var_export(implode('', $from), true) . ',' . var_export(implode('', $to), true);
                }
                else
                {
                    $php .= '[';
                    $cnt = count($from);
                    for ($i = 0; $i < $cnt; ++$i)
                    {
                        if ($i)
                        {
                            $php .= ',';
                        }
                        $php .= var_export($from[$i], true) . '=>' . var_export($to[$i], true);
                    }
                    $php .= ']';
                }
                $php .= ')';
                return $php;
            }
        }
        // If the condition does not seem to contain a relational expression, or start with a
        // function call, we wrap it inside of a string() call
        if (!preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr))
        {
            $expr = 'string(' . $expr . ')';
        }
        // Parse the expression for variables
        $phpTokens = [];
        $pos = 0;
        $len = strlen($expr);
        while ($pos < $len)
        {
            // If we have a string literal, capture it and add its PHP representation
            if ($expr[$pos] === "'" || $expr[$pos] === '"')
            {
                $nextPos = strpos($expr, $expr[$pos], 1 + $pos);
                if ($nextPos === false)
                {
                    throw new RuntimeException('Unterminated string literal in XPath expression ' . var_export($expr, true));
                }
                // Capture the string
                $phpTokens[] = var_export(substr($expr, $pos, $nextPos + 1 - $pos), true);
                // Move the cursor past the string
                $pos = $nextPos + 1;
                continue;
            }
            // Variables in XPath expressions have to be resolved at runtime via getParamAsXPath()
            if ($expr[$pos] === '$' && preg_match('/\\$(\\w+)/', $expr, $m, 0, $pos))
            {
                $phpTokens[] = '$this->getParamAsXPath(' . var_export($m[1], true) . ')';
                $pos += strlen($m[0]);
                continue;
            }
            // Capture everything up to the next interesting character
            $spn = strcspn($expr, '\'"$', $pos);
            if ($spn)
            {
                $phpTokens[] = var_export(substr($expr, $pos, $spn), true);
                $pos += $spn;
            }
        }
        return '$this->xpath->evaluate(' . implode('.', $phpTokens) . ',$node)';
    }
    /**
    * Serialize an <applyTemplates/> node
    *
    * @param  DOMElement $applyTemplates <applyTemplates/> node
    * @return string
    */
    protected function serializeApplyTemplates(DOMElement $applyTemplates)
    {
        $php = '$this->at($node';
        if ($applyTemplates->hasAttribute('select'))
        {
            $php .= ',' . var_export($applyTemplates->getAttribute('select'), true);
        }
        $php .= ');';
        return $php;
    }
    /**
    * Serialize an <attribute/> node
    *
    * @param  DOMElement $attribute <attribute/> node
    * @return string
    */
    protected function serializeAttribute(DOMElement $attribute)
    {
        $attrName = $attribute->getAttribute('name');
        // PHP representation of this attribute's name
        $phpAttrName = $this->convertAttributeValueTemplate($attrName);
        // NOTE: the attribute name is escaped by default to account for dynamically-generated names
        $phpAttrName = 'htmlspecialchars(' . $phpAttrName . ',' . ENT_QUOTES . ')';
        return "\$this->out.=' '." . $phpAttrName . ".'=\"';"
             . $this->serializeChildren($attribute)
             . "\$this->out.='\"';";
    }
    /**
    * Serialize all the children of given node into PHP
    *
    * @param  DOMElement $ir Internal representation
    * @return string
    */
    public function serializeChildren(DOMElement $ir)
    {
        $php = '';
        foreach ($ir->childNodes as $node)
        {
            $methodName = 'serialize' . ucfirst($node->localName);
            $php .= $this->$methodName($node);
        }
        return $php;
    }
    /**
    * Serialize a <closeTag/> node
    *
    * @param  DOMElement $closeTag <closeTag/> node
    * @return string
    */
    protected function serializeCloseTag(DOMElement $closeTag)
    {
        $php = '';
        $id  = $closeTag->getAttribute('id');
        if ($closeTag->hasAttribute('check'))
        {
            $php .= 'if(!isset($t' . $id . ')){';
        }
        if ($closeTag->hasAttribute('set'))
        {
            $php .= '$t' . $id . '=1;';
        }
        // Get the element that's being closed
        $xpath   = new DOMXPath($closeTag->ownerDocument);
        $element = $xpath->query('ancestor::element[@id="' . $id . '"]', $closeTag)->item(0);
        $isVoid  = $element->getAttribute('void');
        $isEmpty = $element->getAttribute('empty');
        if ($this->outputMethod === 'html')
        {
            $php .= "\$this->out.='>';";
            if ($isVoid === 'maybe')
            {
                // Check at runtime whether this element is not void
                $php .= 'if(!$v' . $id . '){';
            }
        }
        else
        {
            // In XML mode, we only care about whether this element is empty
            if ($isEmpty === 'yes')
            {
                // Definitely empty, use a self-closing tag
                $php .= "\$this->out.='/>';";
            }
            else
            {
                // Since it's not definitely empty, we'll close this start tag normally
                $php .= "\$this->out.='>';";
                if ($isEmpty === 'maybe')
                {
                    // Maybe empty, record the length of the output and if it doesn't grow we'll
                    // change the start tag into a self-closing tag
                    $php .= '$l' . $id . '=strlen($this->out);';
                }
            }
        }
        if ($closeTag->hasAttribute('check'))
        {
            $php .= '}';
        }
        return $php;
    }
    /**
    * Serialize a <comment/> node
    *
    * @param  DOMElement $comment <comment/> node
    * @return string
    */
    protected function serializeComment(DOMElement $comment)
    {
        return "\$this->out.='<!--';"
             . $this->serializeChildren($comment)
             . "\$this->out.='-->';";
    }
    /**
    * Serialize a <copyOfAttributes/> node
    *
    * @param  DOMElement $copyOfAttributes <copyOfAttributes/> node
    * @return string
    */
    protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes)
    {
        return 'foreach($node->attributes as $attribute)'
             . '{'
             . "\$this->out.=' ';"
             . "\$this->out.=\$attribute->name;"
             . "\$this->out.='=\"';"
             . "\$this->out.=htmlspecialchars(\$attribute->value," . ENT_COMPAT . ");"
             . "\$this->out.='\"';"
             . '}';
    }
    /**
    * Serialize an <element/> node
    *
    * @param  DOMElement $element <element/> node
    * @return string
    */
    protected function serializeElement(DOMElement $element)
    {
        $php     = '';
        $elName  = $element->getAttribute('name');
        $id      = $element->getAttribute('id');
        $isVoid  = $element->getAttribute('void');
        $isEmpty = $element->getAttribute('empty');
        // Test whether this element name is dynamic
        $isDynamic = (bool) (strpos($elName, '{') !== false);
        // PHP representation of this element's name
        $phpElName = $this->convertAttributeValueTemplate($elName);
        // NOTE: the element name is escaped by default to account for dynamically-generated names
        $phpElName = 'htmlspecialchars(' . $phpElName . ',' . ENT_QUOTES . ')';
        // If the element name is dynamic, we cache its name for convenience and performance
        if ($isDynamic)
        {
            $varName = '$e' . $id;
            // Add the var declaration to the source
            $php .= $varName . '=' . $phpElName . ';';
            // Replace the element name with the var
            $phpElName = $varName;
        }
        // Test whether this element is void if we need this information
        if ($this->outputMethod === 'html' && $isVoid === 'maybe')
        {
            $php .= '$v' . $id . '=preg_match(' . var_export(TemplateParser::$voidRegexp, true) . ',' . $phpElName . ');';
        }
        // Open the start tag
        $php .= "\$this->out.='<'." . $phpElName . ';';
        // Serialize this element's content
        $php .= $this->serializeChildren($element);
        // If we're in XHTML mode and the element is or may be empty, we may not need to close it at
        // all
        if ($this->outputMethod === 'xhtml')
        {
            // If this element is definitely empty, it has already been closed with a self-closing
            // tag in serializeCloseTag()
            if ($isEmpty === 'yes')
            {
                return $php;
            }
            // If this element may be empty, we need to check at runtime whether we turn its start
            // tag into a self-closing tag or append an end tag
            if ($isEmpty === 'maybe')
            {
                $php .= 'if($l' . $id . '===strlen($this->out)){';
                $php .= "\$this->out=substr(\$this->out,0,-1).'/>';";
                $php .= '}else{';
                $php .= "\$this->out.='</'." . $phpElName . ".'>';";
                $php .= '}';
                return $php;
            }
        }
        // Close that element, unless we're in HTML mode and we know it's void
        if ($this->outputMethod !== 'html' || $isVoid !== 'yes')
        {
            $php .= "\$this->out.='</'." . $phpElName . ".'>';";
        }
        // If this element was maybe void, serializeCloseTag() has put its content within an if
        // block. We need to close that block
        if ($this->outputMethod === 'html' && $isVoid === 'maybe')
        {
            $php .= '}';
        }
        return $php;
    }
    /**
    * Unused
    * @todo Remove
    */
    protected function serializeMatch()
    {
        return '';
    }
    /**
    * Serialize an <output/> node
    *
    * @param  DOMElement $output <output/> node
    * @return string
    */
    protected function serializeOutput(DOMElement $output)
    {
        $php        = '';
        $xpath      = new DOMXPath($output->ownerDocument);
        $escapeMode = ($xpath->evaluate('count(ancestor::attribute)', $output))
                    ? ENT_COMPAT
                    : ENT_NOQUOTES;
        if ($output->getAttribute('type') === 'xpath')
        {
            $php .= '$this->out.=htmlspecialchars(';
            $php .= $this->convertXPath($output->textContent);
            $php .= ',' . $escapeMode . ');';
        }
        else
        {
            // Literal
            $php .= '$this->out.=';
            $php .= var_export(htmlspecialchars($output->textContent, $escapeMode), true);
            $php .= ';';
        }
        return $php;
    }
    /**
    * Serialize a <switch/> node
    *
    * @param  DOMElement $switch <switch/> node
    * @return string
    */
    protected function serializeSwitch(DOMElement $switch)
    {
        $php  = '';
        $else = '';
        foreach ($switch->getElementsByTagName('case') as $case)
        {
            if ($case->parentNode !== $switch)
            {
                continue;
            }
            if ($case->hasAttribute('test'))
            {
                $php .= $else . 'if(' . $this->convertCondition($case->getAttribute('test')) . ')';
            }
            else
            {
                $php .= 'else';
            }
            $else = 'else';
            $php .= '{';
            $php .= $this->serializeChildren($case);
            $php .= '}';
        }
        return $php;
    }
}